Skip to content

Autocomplete tags API function - #59

Merged
pomegranited merged 14 commits into
openedx:mainfrom
open-craft:chris/autocomplete-tags
Aug 6, 2023
Merged

Autocomplete tags API function#59
pomegranited merged 14 commits into
openedx:mainfrom
open-craft:chris/autocomplete-tags

Conversation

@ChrisChV

@ChrisChV ChrisChV commented Jun 20, 2023

Copy link
Copy Markdown
Contributor

Description

Implements autocomplete_tags function

Supporting information

Merge after #57.

Closes openedx/modular-learning#65

Testing instructions

  • Ensure that the tests cover the expected behaviour

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Jun 20, 2023
@openedx-webhooks

openedx-webhooks commented Jun 20, 2023

Copy link
Copy Markdown

Thanks for the pull request, @ChrisChV! Please note that it may take us up to several weeks or months to complete a review and merge your PR.

Feel free to add as much of the following information to the ticket as you can:

  • supporting documentation
  • Open edX discussion forum threads
  • timeline information ("this must be merged by XX date", and why that is)
  • partner information ("this is a course on edx.org")
  • any other information that can help Product understand the context for the PR

All technical communication about the code itself will be done via the GitHub pull request interface. As a reminder, our process documentation is here.

Please let us know once your PR is ready for our review and all tests are green.

@ChrisChV
ChrisChV marked this pull request as draft June 20, 2023 20:10
@ChrisChV ChrisChV changed the title Autocomplete tags API function (WIP) Autocomplete tags API function Jun 20, 2023
@ChrisChV
ChrisChV force-pushed the chris/autocomplete-tags branch from 0b9e6f6 to e8f8244 Compare June 21, 2023 00:26
@ChrisChV
ChrisChV marked this pull request as ready for review June 21, 2023 00:27
Comment thread openedx_tagging/core/tagging/api.py Outdated
Comment thread tests/openedx_tagging/core/tagging/test_api.py Outdated
Comment thread tests/openedx_tagging/core/tagging/test_api.py Outdated
Comment thread tests/openedx_tagging/core/tagging/test_api.py Outdated
Comment thread tests/openedx_tagging/core/tagging/test_api.py Outdated
Comment thread openedx_tagging/core/tagging/api.py Outdated
Comment thread openedx_tagging/core/tagging/api.py Outdated
Comment thread openedx_tagging/core/tagging/api.py Outdated
@ChrisChV
ChrisChV force-pushed the chris/autocomplete-tags branch 2 times, most recently from 0642778 to 4923b66 Compare June 21, 2023 21:56
@ChrisChV

Copy link
Copy Markdown
Contributor Author

@pomegranited The CI tests are failing, but in my local works normally. Please let me know your results in your local

@pomegranited

pomegranited commented Jun 22, 2023

Copy link
Copy Markdown
Contributor

@pomegranited The CI tests are failing, but in my local works normally. Please let me know your results in your local

@ChrisChV Hmm.. make test passes for me locally, and I had the same problem on mine, so I think it's a sorting issue, something to do with CI running the tests on mysql?

If you merge my latest changes, it should fix it.

Comment thread openedx_tagging/core/tagging/models.py Outdated
@ChrisChV
ChrisChV force-pushed the chris/autocomplete-tags branch from 4923b66 to 67bb621 Compare June 22, 2023 19:07
@ChrisChV

Copy link
Copy Markdown
Contributor Author

@ChrisChV Hmm.. make test passes for me locally, and I had the same problem on mine, so I think it's a sorting issue, something to do with CI running the tests on mysql?

If you merge my latest changes, it should fix it.

@pomegranited Thanks. MySQL uses __istartswith to make case-insensitive searches.

@ChrisChV

Copy link
Copy Markdown
Contributor Author

@ormsbee It's ready for your review

@ChrisChV
ChrisChV force-pushed the chris/autocomplete-tags branch from 89e6455 to 2815e58 Compare June 27, 2023 22:38
@ChrisChV

Copy link
Copy Markdown
Contributor Author

@ormsbee I merged the changes of #57. Now the review is easier and it's ready

@pomegranited pomegranited left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of minor nits, but I've also got a question about duplicate Tags in a single taxonomy?

Comment thread openedx_tagging/core/tagging/api.py Outdated
Comment thread openedx_tagging/core/tagging/models.py Outdated
Comment thread openedx_tagging/core/tagging/api.py Outdated

@ormsbee ormsbee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly questions and requests around the database queries being made. Thank you!

Comment thread openedx_tagging/core/tagging/api.py Outdated
Comment thread openedx_tagging/core/tagging/api.py Outdated
.order_by('_value')
# obtain the values of the tags
.values_list('_value', flat=True)
# remove repeats

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what scenarios could we get repeats?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the free-form taxonomies, we do the search in ObjectTag. There may be the scenario where different objects have the same tag

Comment thread openedx_tagging/core/tagging/api.py Outdated
[:count]
)
# Build tag results
result = [TagResult(id=None, value=value) for value in result]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is likely to be used by a REST API at some point, I think it's fine to have a function that returns a QuerySet, even if this function just calls out to it and does this serialization into TagResult.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I have added a separate function for serialization, that can be used by other internal functions

Comment thread openedx_tagging/core/tagging/api.py Outdated
Comment on lines +135 to +167
# omit any tags applied to the given object
.exclude(object_id=object_id)
# omit any free-text tags from other objects whose values match the tags on the given object
.exclude(_value__in=excluded_tags)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're omitting everything with the object_id here, why do we need to check excluded_tags as well (which should just be a subset)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the same logic of #59 (comment). There may be tags of other objects that the object_id already has. But I have realized that .exclude(object_id=object_id) is no necessary, because .exclude(_value__in=excluded_tags) also excludes the tags of object_id

@mphilbrick211

Copy link
Copy Markdown

Hi @ChrisChV! Just following up on this :)

@pomegranited

Copy link
Copy Markdown
Contributor

Hi @mphilbrick211 :) This is blocked on #62, but we're nearly there :)

@mphilbrick211

Copy link
Copy Markdown

Hi @mphilbrick211 :) This is blocked on #62, but we're nearly there :)

Thanks, @pomegranited!

@pomegranited pomegranited left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ChrisChV -- I've left some comments in light of the soon-to-be-approved #62

Comment thread tests/openedx_tagging/core/tagging/test_api.py Outdated
Comment thread openedx_tagging/core/tagging/models.py
Comment thread openedx_tagging/core/tagging/api.py Outdated
Comment thread openedx_tagging/core/tagging/api.py Outdated
@ChrisChV
ChrisChV force-pushed the chris/autocomplete-tags branch from adb3e5a to 507848b Compare July 25, 2023 18:06

def autocomplete_tags(
self, prefix: str, object_id: str = None, count=10
) -> models.QuerySet:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's going to be terribly confusing that the QuerySet returned here is a ObjectTag queryset for free-text taxonomies, but a Tag queryset for closed taxonomies. So we can't simply use the API and treat them like the same thing.

Plus, for closed taxonomies, the frontend can handle auto-completion without consulting the backend (e.g using autocompleter), because it's already fetched all of the Tags in a Taxonomy to display the tree of options:

autocomplete

So I think we should:

  • Only autocomplete from existing ObjectTags, ignoring the free-text vs closed taxonomy distinction.
  • Match text anywhere in the value, not just prefix (as shown above), by using .filter(_value__icontains=text) instead
  • Remove the count parameter -- the REST API that ends up calling this can handle pagination from the returned query set.

Does that sound reasonable to you?

@ChrisChV ChrisChV Jul 26, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pomegranited Yes, with this the function will be cleaner. The new function will only return the _value and tag. The second will be null for closed taxonomies

With this change, I wonder, won't there be other things that the frontend can simplify or change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated here a8523b5

@pomegranited pomegranited left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really solid, @ChrisChV .
I've suggested some improvements to the docstrings and an alias to make the returned tag values nicer, but will approve once they're applied.

Comment thread openedx_tagging/core/tagging/models.py
Comment thread tests/openedx_tagging/core/tagging/test_api.py Outdated
Comment thread tests/openedx_tagging/core/tagging/test_api.py Outdated
Comment thread openedx_tagging/core/tagging/models.py Outdated
Comment thread openedx_tagging/core/tagging/api.py Outdated
Comment thread openedx_tagging/core/tagging/models.py Outdated
Comment thread openedx_tagging/core/tagging/models.py

@bradenmacdonald bradenmacdonald left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

While I generally recommend against returning QuerySets in api.py methods*, I think this one makes sense because it's a ValuesQuerySet. I like how the method is returning values and tag, so that we can be more flexible in the future, e.g. returning users even if their Tags don't exist yet.

I have a few questions/suggestions, but nothing blocking.

Nice work.

* Because: "An app’s Django models and other internal data structures should not be exposed via its Python APIs"

Comment thread openedx_tagging/core/tagging/api.py
Comment thread openedx_tagging/core/tagging/api.py
@pomegranited

Copy link
Copy Markdown
Contributor

@bradenmacdonald

While I generally recommend against returning QuerySets in api.py methods because: "An app’s Django models and other internal data structures should not be exposed via its Python APIs"

Huh ok.. I didn't know that, and we definitely should have picked this up in the design phase.

My assumption was that the REST APIs should be as think as possible, and so should use the Python APIs (and django model rules) to encapsulate any complexity. And since we use DRF, that means preferring the Python APIs to return QuerySets.

How should this be done instead?

@pomegranited

Copy link
Copy Markdown
Contributor

@ChrisChV could you rebase this on latest main and bump the version so we can merge?

ChrisChV added 13 commits August 2, 2023 10:56
To return a list of TagResult containing the id and value of the tags.
We need to use __istartswith to make a case-insensitive query on MySQL
- Match text anywhere in the value
- Only autocomplete from existing ObjectTags, ignoring the free-text vs closed taxonomy distinction.
- Remove the count parameter
- Refactoring tests to use ddt
@bradenmacdonald

Copy link
Copy Markdown
Contributor

@pomegranited It's not a hard and fast rule. The main problem the authors of that OEP are trying to avoid is that Django model instances are very "powerful" and once you have access to one, you can make a lot of "internal" changes that may violate the assumptions of the API - for example, you can change fields and save() it, bypassing whatever save_changes() API may exist. Specific example: In the tagging case, we'd generally want people to use the tag_object() API method, not modifying or deleting an ObjectTag instance themselves. In fact, that sort of design is one way to avoid having so many subclasses - by forcing people to use the methods like tagging_api.tag_object(...) you can keep all the extra validation/creation logic in that method, and avoid the need for model subclasses.

So: How can you make the REST APIs a thin wrapper around the python API, without using Django models? You can look at this example in content_libraries v2 as one example I'm familiar with. Other times it may make more sense to use a query set in the REST API if it makes your life a lot easier (e.g. pagination). Yet another option is have an "internal API" that uses QuerySets and then your REST API can wrap it directly and your Python API can wrap it and convert the Django objects to non-Django data structures. As far as I know, any of those are totally fine, so just go with any approach that feels clean to you. Just keep in mind that exposing Django model instances via python will tempt people to bypass your python API, which you probably want to discourage.

@ChrisChV
ChrisChV force-pushed the chris/autocomplete-tags branch from 7c30263 to ea01201 Compare August 2, 2023 16:35
@ChrisChV

ChrisChV commented Aug 2, 2023

Copy link
Copy Markdown
Contributor Author

@ChrisChV could you rebase this on latest main and bump the version so we can merge?

@pomegranited Done 👍

@pomegranited pomegranited left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thank you @ChrisChV !

  • I tested this by checking that the tests correctly cover the expected behavioral use cases.
  • I read through the code.
  • I checked for accessibility issues N/A
  • Includes documentation -- docstrings

@pomegranited

Copy link
Copy Markdown
Contributor

Hi @ormsbee -- have your concerns been addressed? Github won't let me merge until you say it's ok :)

@pomegranited
pomegranited dismissed ormsbee’s stale review August 6, 2023 23:11

I don't want to "dismiss" this review -- Dave's questions were very relevant and appreciated. But they've been answered, and we had a second approval from Braden, so I think it's safe to merge this.

@pomegranited
pomegranited merged commit fb1fd08 into openedx:main Aug 6, 2023
@openedx-webhooks

Copy link
Copy Markdown

@ChrisChV 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

[Tagging] Autocomplete tags API method

6 participants